1 /*--
2 Copyright (C) 2005 Tim Solley.
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions
7 are met:
8
9 1. Redistributions of source code must retain the above copyright
10 notice, this list of conditions, and the following disclaimer.
11
12 2. Redistributions in binary form must reproduce the above copyright
13 notice, this list of conditions, and the disclaimer that follows
14 these conditions in the documentation and/or other materials
15 provided with the distribution.
16
17 3. The name "Deadbolt" may be used to endorse or promote products
18 derived from this software without prior written permission.
19
20 4. Products derived from this software may not be called "Deadbolt", nor
21 may "Deadbolt" appear in their name, without prior written permission
22 from the Deadbolt Project Management timsolley@yahoo.com.
23
24 In addition, we request (but do not require) that you include in the
25 end-user documentation provided with the redistribution and/or in the
26 software itself an acknowledgement equivalent to the following:
27 "This product includes software developed by the
28 Deadbolt Project (http://deadbolt.sourceforge.net/)."
29 Alternatively, the acknowledgment may be graphical using the logos
30 available at http://deadbolt.sourceforge.net.
31
32 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
33 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
34 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
35 DISCLAIMED. IN NO EVENT SHALL THE DEADBOLT AUTHORS OR THE PROJECT
36 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
37 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
38 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
39 USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
40 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
41 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
42 OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
43 SUCH DAMAGE.
44
45 This software consists of voluntary contributions made by many
46 individuals on behalf of the Deadbolt Project and was originally
47 created by Tim Solley timsolley@yahoo.com. For more information
48 on the Deadbolt Project, please see <http://deadbolt.sourceforge.net/>.
49 */
50
51 package net.sf.deadbolt;
52
53 import java.io.File;
54 import java.io.IOException;
55 import java.util.*;
56
57 import javax.servlet.*;
58 import javax.servlet.http.HttpServletRequest;
59 import javax.servlet.http.HttpServletResponse;
60
61 import net.sf.deadbolt.handlers.DeadboltHandler;
62 import net.sf.deadbolt.model.Room;
63 import net.sf.deadbolt.model.URLMapping;
64 import net.sf.deadbolt.tags.DisplayErrorsTag;
65
66 import org.apache.commons.lang.StringUtils;
67 import org.apache.log4j.Logger;
68 import org.jdom.Document;
69 import org.jdom.Element;
70 import org.jdom.JDOMException;
71 import org.jdom.input.SAXBuilder;
72
73
74 /***
75 * This class is the meat of the Deadbolt framework. This filter is executed on
76 * every request for an application, and it determines the authentication and
77 * authorization setup for the requested resource.
78 *
79 * @author Tim Solley <timsolley@yahoo.com>
80 */
81 public class DeadboltFilter implements Filter {
82 /* Jakarta Commons Logging is used here */
83 private static Logger logger = Logger.getLogger(DeadboltFilter.class.getName());
84
85 /* Added to the object in case a handler needs access to the ServletContext */
86 private static ServletContext context;
87
88 /*
89 * This is a list of mappings from URL pattern to a room. This indicates the
90 * room to be used when authenticating a resource.
91 */
92 private static List urlMappings;
93
94 /*
95 * The list of error messages that will be shown to the user if any are
96 * added.
97 */
98 private static Map errorMessages;
99
100 /*
101 * This is the Room to contain global information.
102 */
103 private static Room globalRoom;
104
105 /***
106 * This method will return the <code>Map</code> of error messages, which
107 * are <code>String</code> objects. This is used by the
108 * {@link DeadboltHandler}to put messages in the request for later display
109 * by the {@link DisplayErrorsTag}custom tag on the JSP page.
110 *
111 * @return Returns the errorMessages.
112 */
113 public static Map getErrorMessages() {
114 return errorMessages;
115 }
116
117 /***
118 * This method is the starting point the the Deadbolt enabled application.
119 * It processes the configuration XML file and stores the configuration
120 * information in memory when the application starts up. That information
121 * will be later used by the <code>doFilter</code> method to secure the
122 * requested resource.
123 */
124 public void init(FilterConfig config) throws ServletException {
125 logger.debug("ENTERING: init");
126
127 context = config.getServletContext();
128 /*
129 * Get the config file: The config file is specified in the web.xml file
130 * under an init-param named "config-file" for the filter. We simply use
131 * the application context to the get the path within the application.
132 * If the user wants to put the file outside the application, this part
133 * will not work. This is a good area for someone to make that more
134 * flexible, allowing a user to put the file anywhere, taking the
135 * configuration information out of the application and allowing it to
136 * be modified without having to create another build.
137 */
138 File configFile = new File(context.getRealPath(config
139 .getInitParameter("config-file")));
140
141 /*
142 * Read the XML and do setup work: JDOM is used to process the config
143 * file. This section takes the XML file in pieces, processing each
144 * block sequentially.
145 */
146 SAXBuilder builder = new SAXBuilder();
147 builder.setValidation(true);
148 builder.setFeature("http://apache.org/xml/features/validation/schema", true);
149 urlMappings = new ArrayList();
150
151 try {
152 Document document = builder.build(configFile);
153 Element deadboltConfigElement = document.getRootElement();
154
155 // Process the error messages, starting with the global error page
156 String globalErrorPage = deadboltConfigElement
157 .getChildText("global-error-page");
158
159 /*
160 * Process the handlers: Handlers can be reused within the config
161 * file. So, to avoid redundency, the sets up the unique list of
162 * them. They then just reference the handler name in the room. Here
163 * we just process that list, and store them in memory so that they
164 * can be accessed when processing rooms.
165 */
166 List handlerList = deadboltConfigElement.getChildren("handler");
167 Map handlers = new Hashtable();
168 for (Iterator it = handlerList.iterator(); it.hasNext();) {
169 Element handlerElement = (Element) it.next();
170 handlers.put(handlerElement.getChildText("handler-name"),
171 handlerElement.getChildText("handler-class"));
172 }
173
174 /*
175 * Process the global handlers. These will be run on every request
176 * that Deadbolt responds to, before control is passed off to the
177 * Rooms for processing.
178 */
179 List globalHandlersList = deadboltConfigElement.getChildren("global-handler");
180 if(globalHandlersList.size() != 0) {
181 globalRoom = new Room();
182 TreeMap globalHandlerMap = new TreeMap();
183 Map globalInitParams = new Hashtable();
184 for(Iterator iterator = globalHandlersList.iterator(); iterator.hasNext();) {
185 Element handler = (Element) iterator.next();
186 String handlerClass = (String) handlers.get(handler.getChildText("handler-name"));
187 globalHandlerMap.put(handler.getChildText("execution-order"), handlerClass);
188 logger.debug("Global handler: " + handler.getChildText("execution-order") + " | " +
189 handlerClass);
190
191 /*
192 * Set up the initial parameters for this global handler
193 */
194 List initParamsElements = handler.getChildren("init-param");
195 for (Iterator i = initParamsElements.iterator(); i.hasNext();) {
196 Element initParam = (Element) i.next();
197 // Add the current param to the list
198 globalInitParams.put(initParam.getChildText("param-name"),
199 initParam.getChildText("param-value"));
200 logger.debug("Param name: " + initParam.getChildText("param-name"));
201 logger.debug("Param value: " + initParam.getChildText("param-value"));
202 }
203 }
204 globalRoom.setInitParams(globalInitParams);
205 globalRoom.setHandler(globalHandlerMap);
206 }
207
208
209 /*
210 * Process the rooms.
211 */
212 List roomList = deadboltConfigElement.getChildren("room");
213 for (Iterator it = roomList.iterator(); it.hasNext();) {
214 // Set up a new room
215 Room room = new Room();
216 Map initParams = new Hashtable();
217 TreeMap handlerMap = new TreeMap();
218 Element roomElement = (Element) it.next();
219
220 /*
221 * Set up the handler chain for this room: The user can specify
222 * multiple handlers for a room. When they do this, they will be
223 * executed sequentially when a request for the specified
224 * resource comes in. The user specifies the execution order
225 * using the "execution-order" element. This section reads in
226 * the handler, and adds it to a TreeMap, which will
227 * automatically put it in the correct order based on the
228 * "execution-order".
229 */
230 List handlerListElement = roomElement.getChildren("handler");
231 for (Iterator iter = handlerListElement.iterator(); iter
232 .hasNext();) {
233 Element handler = (Element) iter.next();
234 String handlerClass = (String) handlers.get(handler
235 .getChildText("handler-name"));
236 // Add the handler to the TreeMap, going into the correct
237 // order
238 handlerMap.put(handler.getChildText("execution-order"),
239 handlerClass);
240 logger.debug("Handler: "
241 + handler.getChildText("execution-order") + " | "
242 + handlerClass);
243 }
244 // Add the handler chain to the room
245 room.setHandler(handlerMap);
246
247 /*
248 * Set up the init parameters for this room: Init params can be
249 * defined by the user. They can have as many as they want.
250 * These will be available to the custom handler by way of the
251 * room.
252 */
253 List initParamsElements = roomElement.getChildren("init-param");
254 for (Iterator i = initParamsElements.iterator(); i.hasNext();) {
255 Element initParam = (Element) i.next();
256 // Add the current param to the list
257 initParams.put(initParam.getChildText("param-name"),
258 initParam.getChildText("param-value"));
259 logger.debug("Param name: " + initParam.getChildText("param-name"));
260 logger.debug("Param value: " + initParam.getChildText("param-value"));
261 }
262 // Add the init params to the room
263 room.setInitParams(initParams);
264
265 /*
266 * Set up the error page for this room: If no error page is
267 * specified, then the global error page will be used.
268 */
269 String errorPage = roomElement.getChildText("error-page");
270 if (errorPage == null) {
271 room.setErrorPage(globalErrorPage);
272 } else {
273 room.setErrorPage(errorPage);
274 }
275
276 /*
277 * Set up the URL mappings for this room: This will be the
278 * mechanism that matches a URL pattern to a room.
279 */
280 List urls = roomElement.getChildren("url-pattern");
281 for (Iterator i = urls.iterator(); i.hasNext();) {
282 URLMapping mapping = new URLMapping();
283 mapping.setRoom(room);
284 // Get the URL pattern from the config file
285 String pattern = ((Element) i.next()).getText();
286 /*
287 * The requested pattern will be tested against this pattern
288 * later on in the doFilter method using a regular
289 * expression. To do this, we need to ensure that any
290 * asterisks entered by the user are translated to a regular
291 * express character. So we replace stars with ".++", for
292 * zero to infinity of any character.
293 */
294 pattern = StringUtils.replace(pattern, "*", ".++");
295 mapping.setUrlPattern(pattern);
296 // Add this mapping to the list of mappings
297 urlMappings.add(mapping);
298 }
299 }
300
301 /*
302 * Set up the list of error message keys: These keys are used by the
303 * user in the handlers to send error messages to the client using
304 * consistent verbiage. This also enables the user to make spelling,
305 * grammatical, or wording changes in one place, outside code,
306 * without having to recompile the code.
307 */
308 errorMessages = new Hashtable();
309 Element errorMessagesElement = deadboltConfigElement
310 .getChild("error-messages");
311 List errorMessageList = errorMessagesElement
312 .getChildren("error-message");
313 for (Iterator it = errorMessageList.iterator(); it.hasNext();) {
314 Element errorMessage = (Element) it.next();
315 errorMessages.put(errorMessage.getChildText("message-key"),
316 errorMessage.getChildText("message-content"));
317 }
318 } catch (IOException ioe) {
319 logger.warn("An IOException occurred during init: "
320 + ioe.getMessage());
321 } catch (JDOMException jde) {
322 logger.warn("A JDOM Exception occurred during init: " + jde.getMessage());
323 }
324 logger.debug("EXITING: init");
325 }
326
327 /***
328 * This method will be called on each request to the application, provided
329 * the user used <code>/*</code> as the url-pattern element in the web.xml
330 * file. This method will examine the request, compare it's URL to a list of
331 * URL patterns defined in the config file, and execute handlers based on
332 * any room found that matches the URL pattern. It will then let the user
333 * pass to the requested resource, or send the user to the specified error
334 * page if not allowed. If any handler sends back a false, the user will not
335 * be permitted to enter.
336 *
337 * It should be noted that if a resource exists, and it's URL pattern is not
338 * specified in the config file, and a blanket <code>/*</code> is not
339 * used, a client will be allowed to access the resource and will not be
340 * protected by Deadbolt.
341 */
342 public void doFilter(ServletRequest request, ServletResponse response,
343 FilterChain chain) throws IOException, ServletException {
344 logger.debug("ENTERING: doFilter");
345 // Cast the request and response so HTTP specific methods can be used
346 HttpServletRequest httpRequest = (HttpServletRequest) request;
347 HttpServletResponse httpResponse = (HttpServletResponse) response;
348 Room room = null;
349
350 // This is a flag to specify if the request will be allowed in or not
351 boolean forwardRequest = false;
352
353 /*
354 * Before we execute any URL specific Rooms, check for a global Room
355 * and execute it first.
356 */
357 if(globalRoom != null) {
358 try {
359 DeadboltHandler handler = null;
360 Collection handlers = globalRoom.getHandlers().values();
361 for(Iterator handlerIterator = handlers.iterator();
362 handlerIterator.hasNext();) {
363 String handlerClass = (String) handlerIterator.next();
364 logger.debug("Executing global handler: " + handlerClass);
365 /*
366 * Since we don't know the class of the actual handler,
367 * we're using the DeadboltHandler class, which all handlers
368 * extend. We instantiate the actual class using the String
369 * name.
370 */
371 handler = (DeadboltHandler) Class.forName(handlerClass)
372 .newInstance();
373
374 // Here's the meat. Do the work on this handler.
375 forwardRequest = handler.authenticate(httpRequest,
376 httpResponse, globalRoom);
377 logger.debug("Global handler returned: " + forwardRequest);
378 if (!forwardRequest)
379 break;
380 }
381 } catch (InstantiationException ie) {
382 logger.warn(ie);
383 throw new ServletException(ie);
384 } catch (IllegalAccessException iae) {
385 logger.warn(iae);
386 throw new ServletException(iae);
387 } catch (ClassNotFoundException cnfe) {
388 logger.warn(cnfe);
389 throw new ServletException(cnfe);
390 }
391 }
392
393 // This is the URL pattern of the request that just came in
394 String currentPattern = httpRequest.getServletPath();
395 // This is a flag to identify if a matching room is found
396 boolean matchFound = false;
397
398 /*
399 * Iterate over the mappings to find the handlers to execute on this
400 * request On each pass, compare the URL pattern in the mapping to that
401 * of the request, and set the matchFound flag to true and set the room
402 * if something is found.
403 */
404 for (Iterator it = urlMappings.iterator(); it.hasNext();) {
405 URLMapping mapping = (URLMapping) it.next();
406 // Use a regular expression to compare the URL for the request to
407 // the URL in the mapping
408 if (currentPattern.matches(mapping.getUrlPattern())) {
409 matchFound = true;
410 // Set the room to be used below
411 room = mapping.getRoom();
412 break;
413 }
414 }
415
416 // If a match was found, execute the handlers for that room
417 if(!matchFound) {
418 forwardRequest = true;
419 } else if(forwardRequest & room != null) {
420 try {
421 DeadboltHandler handler = null;
422 // Get a Collection, because TreeMap has no Iterator or the like
423 Collection handlers = room.getHandlers().values();
424 /*
425 * Iterate over the handler chain for this room, calling the
426 * authenticate method on each one. If any handler returns a
427 * false, then set forwardRequest to false and exit the loop.
428 */
429 for (Iterator i = handlers.iterator(); i.hasNext();) {
430 String handlerClass = (String) i.next();
431 logger.debug("Executing handler: " + handlerClass);
432 /*
433 * Since we don't know the class of the actual handler,
434 * we're using the DeadboltHandler class, which all handlers
435 * extend. We instantiate the actual class using the String
436 * name.
437 */
438 handler = (DeadboltHandler) Class.forName(handlerClass)
439 .newInstance();
440
441 // Here's the meat. Do the work on this handler.
442 forwardRequest = handler.authenticate(httpRequest,
443 httpResponse, room);
444 logger.debug("Handler returned: " + forwardRequest);
445 if (!forwardRequest)
446 break;
447 }
448 } catch (InstantiationException ie) {
449 logger.warn(ie);
450 throw new ServletException(ie);
451 } catch (IllegalAccessException iae) {
452 logger.warn(iae);
453 throw new ServletException(iae);
454 } catch (ClassNotFoundException cnfe) {
455 logger.warn(cnfe);
456 throw new ServletException(cnfe);
457 }
458 }
459
460 /*
461 * If the user is authenticated and authorized, send the request off to
462 * the resource. Otherwise, send the user to the error page defined for
463 * the room.
464 */
465 if (forwardRequest) {
466 logger.debug("The request was approved. Continuing to resource.");
467 chain.doFilter(request, response);
468 } else {
469 logger.debug("The request was denied. Forwarding to error page.");
470 if (room.getErrorPage() != null) {
471 RequestDispatcher dispatcher = httpRequest
472 .getRequestDispatcher(room.getErrorPage());
473 dispatcher.forward(request, response);
474 }
475 }
476 logger.debug("EXITING: doFilter");
477 }
478
479 /***
480 * This is here for compliance with the <code>Filter</code> interface.
481 * Nothing is done in this method.
482 */
483 public void destroy() {
484 }
485 }